use ruma::{
    api::client::{
        account::{
            get_username_availability,
            register::{self, RegistrationKind},
        },
        error::ErrorKind,
        session::{
            get_login_types::{self, v3::PasswordLoginType},
            login,
        },
        uiaa::UserIdentifier,
    },
    OwnedDeviceId, UserId,
};
use tracing::{self, info, warn};

use crate::{
    server::server,
    utils::{self, error::Error, ruma_wrapper::Ruma},
    Result, DEVICE_ID_LENGTH, RANDOM_USER_ID_LENGTH, TOKEN_LENGTH,
};

/// # `POST /_matrix/client/v3/register`
///
/// Register an account on this homeserver.
///
/// You can use [`GET
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available.
///
/// - Only works if registration is enabled
/// - If type is guest: ignores all parameters except
///   initial_device_display_name
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
/// - If type is not guest and no username is given: Always fails after UIAA
///   check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and
///   access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, name = "register")]
pub async fn register_route(
    body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response, Error> {
    let is_guest = body.kind == RegistrationKind::Guest;
    if is_guest {
        return Err(Error::BadRequest(
            ErrorKind::forbidden(),
            "Do not support guest registration.",
        ));
    }
    let user_id = match &body.username {
        Some(username) => {
            let proposed_user_id = UserId::parse_with_server_name(
                username.to_lowercase(),
                &server().config.server_name,
            )
            .ok()
            .ok_or(Error::BadRequest(
                ErrorKind::InvalidUsername,
                "Username is invalid.",
            ))?;

            if server().users.exists(&proposed_user_id)? {
                return Err(Error::BadRequest(
                    ErrorKind::UserInUse,
                    "Desired user ID is already taken.",
                ));
            }

            proposed_user_id
        }
        None => loop {
            let proposed_user_id = UserId::parse_with_server_name(
                utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
                &server().config.server_name,
            )
            .unwrap();
            if !server().users.exists(&proposed_user_id)? {
                break proposed_user_id;
            }
        },
    };

    // UIAA
    // let mut uiaainfo;
    // let skip_auth = if services.globals.config.registration_token.is_some() {
    //     // Registration token required
    //     uiaainfo = UiaaInfo {
    //         flows: vec![AuthFlow {
    //             stages: vec![AuthType::RegistrationToken],
    //         }],
    //         completed: Vec::new(),
    //         params: Box::default(),
    //         session: None,
    //         auth_error: None,
    //     };
    //     body.appservice_info.is_some()
    // } else {
    //     // No registration token necessary, but clients must still go through the flow
    //     uiaainfo = UiaaInfo {
    //         flows: vec![AuthFlow {
    //             stages: vec![AuthType::Dummy],
    //         }],
    //         completed: Vec::new(),
    //         params: Box::default(),
    //         session: None,
    //         auth_error: None,
    //     };
    //     body.appservice_info.is_some() || is_guest
    // };

    // if !skip_auth {
    //     if let Some(auth) = &body.auth {
    //         let (worked, uiaainfo) = services.uiaa.try_auth(
    //             &UserId::parse_with_server_name("", services.globals.server_name())
    //                 .expect("we know this is valid"),
    //             "".into(),
    //             auth,
    //             &uiaainfo,
    //         )?;
    //         if !worked {
    //             return Err(Error::Uiaa(uiaainfo));
    //         }
    //     // Success!
    //     } else if let Some(json) = body.json_body {
    //         uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
    //         services.uiaa.create(
    //             &UserId::parse_with_server_name("", services.globals.server_name())
    //                 .expect("we know this is valid"),
    //             "".into(),
    //             &uiaainfo,
    //             &json,
    //         )?;
    //         return Err(Error::Uiaa(uiaainfo));
    //     } else {
    //         return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
    //     }
    // }

    let password = body.password.as_deref();

    // Create user
    server().users.create_user(&user_id, password)?;

    // Default to pretty displayname
    let _display_name = user_id.localpart().to_owned();

    // If `new_user_displayname_suffix` is set, registration will push whatever
    // content is set to the user's display name with a space before it
    // if !services.globals.new_user_displayname_suffix().is_empty() && body.appservice_info.is_none()
    // {
    //     write!(
    //         displayname,
    //         " {}",
    //         services.globals.config.new_user_displayname_suffix
    //     )
    //     .expect("should be able to write to string buffer");
    // }

    // services
    //     .users
    //     .set_displayname(&user_id, Some(displayname.clone()))
    //     .await?;

    // Initial account data
    // services.account_data.update(
    //     None,
    //     &user_id,
    //     GlobalAccountDataEventType::PushRules.to_string().into(),
    //     &serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
    //         content: ruma::events::push_rules::PushRulesEventContent {
    //             global: push::Ruleset::server_default(&user_id),
    //         },
    //     })
    //     .expect("to json always works"),
    // )?;

    // Inhibit login does not work for guests
    if body.inhibit_login {
        return Ok(register::v3::Response {
            access_token: None,
            user_id,
            device_id: None,
            refresh_token: None,
            expires_in: None,
        });
    }

    // Generate new device id if the user didn't specify one
    // let device_id = if is_guest {
    //     None
    // } else {
    //     body.device_id.clone()
    // }
    // .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());

    // // Generate new token for the device
    // let token = utils::random_string(TOKEN_LENGTH);

    // // Create device for this account
    // services.users.create_device(
    //     &user_id,
    //     &device_id,
    //     &token,
    //     body.initial_device_display_name.clone(),
    //     Some(client.to_string()),
    // )?;

    // debug_info!(%user_id, %device_id, "User account was created");

    // let device_display_name = body.initial_device_display_name.clone().unwrap_or_default();

    // // log in conduit admin channel if a non-guest user registered
    // if body.appservice_info.is_none() && !is_guest {
    //     if !device_display_name.is_empty() {
    //         info!("New user \"{user_id}\" registered on this server with device display name: {device_display_name}");
    //         services
    //             .admin
    //             .send_message(RoomMessageEventContent::notice_plain(format!(
    // 				"New user \"{user_id}\" registered on this server from IP {client} and device display name \
    // 				 \"{device_display_name}\""
    // 			)))
    //             .await;
    //     } else {
    //         info!("New user \"{user_id}\" registered on this server.");
    //         services
    //             .admin
    //             .send_message(RoomMessageEventContent::notice_plain(format!(
    //                 "New user \"{user_id}\" registered on this server from IP {client}"
    //             )))
    //             .await;
    //     }
    // }

    // // log in conduit admin channel if a guest registered
    // if body.appservice_info.is_none() && is_guest && services.globals.log_guest_registrations() {
    //     info!("New guest user \"{user_id}\" registered on this server.");

    //     if !device_display_name.is_empty() {
    //         services
    // 			.admin
    // 			.send_message(RoomMessageEventContent::notice_plain(format!(
    // 				"Guest user \"{user_id}\" with device display name \"{device_display_name}\" registered on this \
    // 				 server from IP {client}"
    // 			)))
    // 			.await;
    //     } else {
    //         services
    // 			.admin
    // 			.send_message(RoomMessageEventContent::notice_plain(format!(
    // 				"Guest user \"{user_id}\" with no device display name registered on this server from IP {client}",
    // 			)))
    // 			.await;
    //     }
    // }

    // // If this is the first real user, grant them admin privileges except for guest
    // // users Note: the server user, @conduit:servername, is generated first
    // if !is_guest {
    //     if let Some(admin_room) = services.admin.get_admin_room()? {
    //         if services.rooms.state_cache.room_joined_count(&admin_room)? == Some(1) {
    //             services.admin.make_user_admin(&user_id).await?;

    //             warn!("Granting {user_id} admin privileges as the first user");
    //         }
    //     }
    // }

    // if body.appservice_info.is_none()
    //     && !services.globals.config.auto_join_rooms.is_empty()
    //     && (services.globals.allow_guests_auto_join_rooms() || !is_guest)
    // {
    //     for room in &services.globals.config.auto_join_rooms {
    //         if !services
    //             .rooms
    //             .state_cache
    //             .server_in_room(services.globals.server_name(), room)?
    //         {
    //             warn!("Skipping room {room} to automatically join as we have never joined before.");
    //             continue;
    //         }

    //         if let Some(room_id_server_name) = room.server_name() {
    //             if let Err(e) = join_room_by_id_helper(
    //                 &services,
    //                 &user_id,
    //                 room,
    //                 Some("Automatically joining this room upon registration".to_owned()),
    //                 &[
    //                     room_id_server_name.to_owned(),
    //                     services.globals.server_name().to_owned(),
    //                 ],
    //                 None,
    //                 &body.appservice_info,
    //             )
    //             .await
    //             {
    //                 // don't return this error so we don't fail registrations
    //                 error!("Failed to automatically join room {room} for user {user_id}: {e}");
    //             } else {
    //                 info!("Automatically joined room {room} for user {user_id}");
    //             };
    //         }
    //     }
    // }

    let token = utils::random_string(TOKEN_LENGTH);
    let device_id = body
        .device_id
        .clone()
        .or(Some(OwnedDeviceId::from(utils::random_string(
            DEVICE_ID_LENGTH,
        ))))
        .unwrap();

    server()
        .users
        .create_device(&user_id, &device_id, token.as_str(), None)?;

    server().users.set_token(&token, &user_id, &device_id)?;

    Ok(register::v3::Response {
        access_token: Some(token),
        user_id,
        device_id: Some(device_id),
        refresh_token: None,
        expires_in: None,
    })
}

pub async fn get_register_available_route(
    body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
    // Validate user id
    let user_id =
        UserId::parse_with_server_name(body.username.to_lowercase(), &server().config.server_name)
            .ok()
            .filter(|user_id| !user_id.is_historical() && server().config.user_is_local(user_id))
            .ok_or(Error::BadRequest(
                ErrorKind::InvalidUsername,
                "Username is invalid.",
            ))?;

    // Check if username is creative enough
    if server().users.exists(&user_id)? {
        return Err(Error::BadRequest(
            ErrorKind::UserInUse,
            "Desired user ID is already taken.",
        ));
    }

    // If no if check is true we have an username that's available to be used.
    Ok(get_username_availability::v3::Response { available: true })
}

pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Response, Error> {
    let user_id = match &body.login_info {
        login::v3::LoginInfo::Password(login::v3::Password {
            identifier,
            password,
            user,
            address,
            medium,
        }) => {
            let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
                UserId::parse_with_server_name(user_id.to_lowercase(), &server().config.server_name)
            } else if let Some(user) = user {
                UserId::parse(user)
            } else {
                warn!("Bad login type: {:?}", &body.login_info);
                return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
            }
            .map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;

            let (password_hash, _salt) =
                server()
                    .users
                    .password_hash(&user_id)?
                    .ok_or(Error::BadRequest(
                        ErrorKind::forbidden(),
                        "Wrong username or password: hash failed.",
                    ))?;

            if password_hash.is_empty() {
                return Err(Error::BadRequest(
                    ErrorKind::UserDeactivated,
                    "The user has been deactivated",
                ));
            }

            let hash_matches =
                argon2::verify_encoded(&password_hash, password.as_bytes()).unwrap_or(false);

            if !hash_matches {
                return Err(Error::BadRequest(
                    ErrorKind::forbidden(),
                    "Wrong username or password: hash mismatch.",
                ));
            }

            user_id
        }
        login::v3::LoginInfo::Token(login::v3::Token { token: _ }) => {
            return Err(Error::BadRequest(
                ErrorKind::Unknown,
                "Token login is not supported (server has no jwt decoding key).",
            ));
        }
        _ => {
            warn!("Unsupported or unknown login type: {:?}", &body.login_info);
            return Err(Error::BadRequest(
                ErrorKind::Unknown,
                "Unsupported login type.",
            ));
        }
    };

    // Generate new device id if the user didn't specify one
    let device_id = body
        .device_id
        .clone()
        .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());

    // Generate a new token for the device
    let token = utils::random_string(TOKEN_LENGTH);

    // Determine if device_id was provided and exists in the db for this user
    let device_exists = body.device_id.as_ref().map_or(false, |device_id| {
        server()
            .users
            .all_device_ids(&user_id)
            .unwrap()
            .iter()
            .any(|x| x == device_id)
    });

    if device_exists {
        server()
            .users
            .set_token(token.as_str(), &user_id, &device_id)?;
        info!("{} logged in, set token {}", user_id, token);
    } else {
        server()
            .users
            .create_device(&user_id, &device_id, &token, None)?;
    }

    info!("{} logged in", user_id);

    // Homeservers are still required to send the `home_server` field
    #[allow(deprecated)]
    Ok(login::v3::Response {
        user_id,
        access_token: token,
        home_server: Some(server().config.server_name.to_owned()),
        device_id,
        well_known: None,
        refresh_token: None,
        expires_in: None,
    })
}

/// # `GET /_matrix/client/v3/login`
///
/// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in.
pub async fn get_login_types_route(
    _body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
    Ok(get_login_types::v3::Response::new(vec![
        get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
    ]))
}
